package jmine.tec.script.persistent.context.impl;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import jmine.tec.script.DecomposableScriptExecutionContext;
import jmine.tec.script.ScriptDefinition;
import jmine.tec.script.ScriptExecutor;
import jmine.tec.script.ScriptExecutorException;
import jmine.tec.script.ScriptParameters;
import jmine.tec.script.persistent.ScriptController;
import jmine.tec.script.persistent.context.PersistentScriptExecutionContext;
import jmine.tec.script.persistent.context.UserExecutionContext;
import jmine.tec.script.persistent.domain.Script;
import jmine.tec.script.persistent.domain.ScriptDependency;

/**
 * Contexto de execução de scripts persistidos.
 * 
 * @author lundberg lundberg
 * @param <R> tipo de resultado retornado
 */
public abstract class AbstractScriptExecutionContext<R> implements PersistentScriptExecutionContext<R>, UserExecutionContext<R>,
        DecomposableScriptExecutionContext<R> {

    private Script script;

    private String dispatcherKey;

    private ScriptParameters parameters;

    private ScriptController scriptController;

    private Class<? extends ScriptExecutor> executorClass;

    /**
     * Construtor protegido, deve ser utilizado apenas pela fábrica de contextos.
     */
    protected AbstractScriptExecutionContext() {
        super();
    }

    /**
     * {@inheritDoc}
     */
    public R dispatchExecution() throws ScriptExecutorException {
        return this.scriptController.getDispatcher().dispatchExecution(this);
    }

    /**
     * {@inheritDoc}
     */
    public R dispatchGetDependencies() throws ScriptExecutorException {
        return this.scriptController.getDispatcher().dispatchGetDependencies(this);
    }

    /**
     * {@inheritDoc}
     */
    public R dispatchGetValidations() throws ScriptExecutorException {
        return this.scriptController.getDispatcher().dispatchGetValidations(this);
    }

    /**
     * {@inheritDoc}
     */
    public void afterExecution() {
        // OK
    }

    /**
     * {@inheritDoc}
     */
    public void beforeExecution() {
        // OK
    }

    /**
     * {@inheritDoc}
     */
    public void setScript(Script script) {
        this.script = script;
        this.injectDependencies();
    }

    /**
     * {@inheritDoc}
     */
    public String getInitScript() {
        return this.assembleScript(this.script.getInitScript());
    }

    /**
     * {@inheritDoc}
     */
    public ScriptParameters getParameters() {
        return this.parameters;
    }

    /**
     * {@inheritDoc}
     */
    public String getScript() {
        return this.assembleScript(this.script);
    }

    /**
     * {@inheritDoc}
     */
    public Serializable getScriptId() {
        return this.script.getId();
    }

    /**
     * {@inheritDoc}
     */
    public String getScriptName() {
        return this.script.getMnemonico();
    }

    /**
     * {@inheritDoc}
     */
    public String getDispatcherKey() {
        return this.dispatcherKey;
    }

    /**
     * {@inheritDoc}
     */
    public Long getVersion() {
        return this.script.getVersion();
    }

    /**
     * {@inheritDoc}
     */
    public Class<? extends ScriptExecutor> getScriptExecutorClass() {
        return this.executorClass;
    }

    /**
     * @param parameters the parameters to set
     */
    public final void setParameters(ScriptParameters parameters) {
        this.parameters = parameters;
        this.injectDependencies();
    }

    /**
     * @param dispatcherKey the dispatcherKey to set
     */
    public void setDispatcherKey(String dispatcherKey) {
        this.dispatcherKey = dispatcherKey;
    }

    /**
     * @param executorClass the executorClass to set
     */
    public final void setExecutorClass(Class<? extends ScriptExecutor> executorClass) {
        this.executorClass = executorClass;
    }

    /**
     * {@inheritDoc}
     */
    public void setScriptController(ScriptController controller) {
        this.scriptController = controller;
        this.injectDependencies();
    }

    /**
     * Monta o script, concatenando as apis e adicionando as dependências aos parâmetros do script.
     * 
     * @param src script
     * @return corpo do script montado
     */
    private String assembleScript(Script src) {
        StringBuilder builder = new StringBuilder();
        if (src != null) {
            this.assembleScriptRecursion(src, builder, new HashSet<Script>());
        }
        return builder.toString();
    }

    /**
     * Recursão utilizada pela montagem do script.
     * 
     * @param src script
     * @param builder builder
     * @param assembled assembled
     */
    private void assembleScriptRecursion(Script src, StringBuilder builder, Set<Script> assembled) {
        for (Script api : src.getApis()) {
            this.assembleScriptRecursion(api, builder, assembled);
        }
        if (assembled.contains(src)) {
            return;
        }
        builder.append(src.getBody());
        assembled.add(src);
    }

    /**
     * Injects all necessary dependencies.
     */
    private void injectDependencies() {
        if (this.script == null || this.scriptController == null || this.getParameters() == null) {
            return;
        }
        this.injectDependenciesRecursion(this.script.getInitScript(), new HashSet<Script>());
        this.injectDependenciesRecursion(this.script, new HashSet<Script>());
    }

    /**
     * Injects all necessary dependencies to this script.
     * 
     * @param src src
     * @param assembled assembled
     */
    private void injectDependenciesRecursion(Script src, Set<Script> assembled) {
        if (src == null) {
            return;
        }
        for (Script api : src.getApis()) {
            this.injectDependenciesRecursion(api, assembled);
        }
        if (assembled.contains(src)) {
            return;
        }
        for (ScriptDependency dependency : src.getDependencies()) {
            Object bean = this.scriptController.findDependency(dependency);
            this.getParameters().putParameter(dependency.getVariable(), bean);
        }
        assembled.add(src);
    }

    /**
     * {@inheritDoc}
     */
    public ScriptDefinition getInitializerScriptDefinition() {
        return this.script.getInitScript();
    }

    /**
     * {@inheritDoc}
     */
    public ScriptDefinition getScriptDefinition() {
        return this.script;
    }

    /**
     * {@inheritDoc}
     */
    public Map<String, Class<?>> getScriptParameterTypes() {
        if (this.parameters == null) {
            return Collections.emptyMap();
        }
        return this.parameters.getParameterDefinitions();
    }

}
